/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
/**
 * @author Oleg V. Khaschansky
 */
package org.apache.harmony.awt.gl.color;

import java.util.ArrayList;

import org.apache.harmony.awt.internal.nls.Messages;

import com.jgraph.gaeawt.java.awt.image.BufferedImage;
import com.jgraph.gaeawt.java.awt.image.ColorModel;
import com.jgraph.gaeawt.java.awt.image.Raster;
import com.jgraph.gaeawt.java.awt.image.SampleModel;
import com.jgraph.gaeawt.java.awt.image.SinglePixelPackedSampleModel;

/**
 * This class converts java color/sample models to the LCMS pixel formats.
 * It also encapsulates all the information about the image format, which native CMM
 * needs to have in order to read/write data.
 *
 * At present planar formats (multiple bands) are not supported
 * and they are handled as a common (custom) case.
 * Samples other than 1 - 7 bytes and multiple of 8 bits are
 * also handled as custom (and won't be supported in the nearest future).
 */
class NativeImageFormat
{
	//////////////////////////////////////////////
	//  LCMS Pixel types
	private static final int PT_ANY = 0; // Don't check colorspace

	// 1 & 2 are reserved
	private static final int PT_GRAY = 3;

	private static final int PT_RGB = 4;

	// Skipping other since we don't use them here
	///////////////////////////////////////////////

	// Conversion of predefined BufferedImage formats to LCMS formats
	private static final int INT_RGB_LCMS_FMT = colorspaceSh(PT_RGB)
			| extraSh(1) | channelsSh(3) | bytesSh(1) | doswapSh(1)
			| swapfirstSh(1);

	private static final int INT_ARGB_LCMS_FMT = INT_RGB_LCMS_FMT;

	private static final int INT_BGR_LCMS_FMT = colorspaceSh(PT_RGB)
			| extraSh(1) | channelsSh(3) | bytesSh(1);

	private static final int THREE_BYTE_BGR_LCMS_FMT = colorspaceSh(PT_RGB)
			| channelsSh(3) | bytesSh(1) | doswapSh(1);

	private static final int FOUR_BYTE_ABGR_LCMS_FMT = colorspaceSh(PT_RGB)
			| extraSh(1) | channelsSh(3) | bytesSh(1) | doswapSh(1);

	private static final int BYTE_GRAY_LCMS_FMT = colorspaceSh(PT_GRAY)
			| channelsSh(1) | bytesSh(1);

	// LCMS format packed into 32 bit value. For description
	// of this format refer to LCMS documentation.
	private int cmmFormat = 0;

	// Dimensions
	private int rows = 0;

	private int cols = 0;

	//  Scanline may contain some padding in the end
	private int scanlineStride = -1;

	private Object imageData;

	// It's possible to have offset from the beginning of the array
	private int dataOffset;

	// Has the image alpha channel? If has - here its band band offset goes
	private int alphaOffset = -1;

	// initializes proper field IDs
	private static native void initIDs();

	static
	{
		NativeCMM.loadCMM();
		initIDs();
	}

	////////////////////////////////////
	// LCMS image format encoders
	////////////////////////////////////
	private static int colorspaceSh(int s)
	{
		return (s << 16);
	}

	private static int swapfirstSh(int s)
	{
		return (s << 14);
	}

	private static int flavorSh(int s)
	{
		return (s << 13);
	}

	private static int planarSh(int s)
	{
		return (s << 12);
	}

	private static int endianSh(int s)
	{
		return (s << 11);
	}

	private static int doswapSh(int s)
	{
		return (s << 10);
	}

	private static int extraSh(int s)
	{
		return (s << 7);
	}

	private static int channelsSh(int s)
	{
		return (s << 3);
	}

	private static int bytesSh(int s)
	{
		return s;
	}

	////////////////////////////////////
	// End of LCMS image format encoders
	////////////////////////////////////

	// Accessors
	Object getChannelData()
	{
		return imageData;
	}

	int getNumCols()
	{
		return cols;
	}

	int getNumRows()
	{
		return rows;
	}

	// Constructors
	public NativeImageFormat()
	{
	}

	/**
	 * Simple image layout for common case with
	 * not optimized workflow.
	 *
	 * For hifi colorspaces with 5+ color channels imgData
	 * should be <code>byte</code> array.
	 *
	 * For common colorspaces with up to 4 color channels it
	 * should be <code>short</code> array.
	 *
	 * Alpha channel is handled by caller, not by CMS.
	 *
	 * Color channels are in their natural order (not BGR but RGB).
	 *
	 * @param imgData - array of <code>byte</code> or <code>short</code>
	 * @param nChannels - number of channels
	 * @param nRows - number of scanlines in the image
	 * @param nCols - number of pixels in one row of the image
	 */
	public NativeImageFormat(Object imgData, int nChannels, int nRows, int nCols)
	{
		if (imgData instanceof short[])
		{
			cmmFormat |= bytesSh(2);
		}
		else if (imgData instanceof byte[])
		{
			cmmFormat |= bytesSh(1);
		}
		else
			// awt.47=First argument should be byte or short array
			throw new IllegalArgumentException(Messages.getString("awt.47")); //$NON-NLS-1$

		cmmFormat |= channelsSh(nChannels);

		rows = nRows;
		cols = nCols;

		imageData = imgData;

		dataOffset = 0;
	}

	/**
	 * Deduces image format from the buffered image type
	 * or color and sample models.
	 * @param bi - image
	 * @return image format object
	 */
	public static NativeImageFormat createNativeImageFormat(BufferedImage bi)
	{
		NativeImageFormat fmt = new NativeImageFormat();

		switch (bi.getType())
		{
			case BufferedImage.TYPE_INT_RGB:
			{
				fmt.cmmFormat = INT_RGB_LCMS_FMT;
				break;
			}

			case BufferedImage.TYPE_INT_ARGB:
			{
				fmt.cmmFormat = INT_ARGB_LCMS_FMT;
				fmt.alphaOffset = 3;
				break;
			}

			default:
				break; // Try to look at sample model and color model
		}

		if (fmt.cmmFormat == 0)
		{
			ColorModel cm = bi.getColorModel();
			SampleModel sm = bi.getSampleModel();

			if (sm instanceof SinglePixelPackedSampleModel)
			{
				SinglePixelPackedSampleModel sppsm = (SinglePixelPackedSampleModel) sm;
				fmt.cmmFormat = getFormatFromSPPSampleModel(sppsm,
						cm.hasAlpha());
				fmt.scanlineStride = calculateScanlineStrideSPPSM(sppsm,
						bi.getRaster());
			}

			if (cm.hasAlpha())
				fmt.alphaOffset = calculateAlphaOffset(sm, bi.getRaster());
		}

		if (fmt.cmmFormat == 0)
			return null;

		if (!fmt.setImageData(bi.getRaster().getDataBuffer()))
		{
			return null;
		}

		fmt.rows = bi.getHeight();
		fmt.cols = bi.getWidth();

		fmt.dataOffset = 0;

		return fmt;
	}

	/**
	 * Deduces image format from the raster sample model.
	 * @param r - raster
	 * @return image format object
	 */
	public static NativeImageFormat createNativeImageFormat(Raster r)
	{
		NativeImageFormat fmt = new NativeImageFormat();
		SampleModel sm = r.getSampleModel();

		if (sm instanceof SinglePixelPackedSampleModel)
		{
			SinglePixelPackedSampleModel sppsm = (SinglePixelPackedSampleModel) sm;
			fmt.cmmFormat = getFormatFromSPPSampleModel(sppsm, false);
			fmt.scanlineStride = calculateScanlineStrideSPPSM(sppsm, r);
		}

		if (fmt.cmmFormat == 0)
			return null;

		fmt.cols = r.getWidth();
		fmt.rows = r.getHeight();
		fmt.dataOffset = 0;

		if (!fmt.setImageData(r.getDataBuffer()))
			return null;

		return fmt;
	}

	/**
	 * Obtains LCMS format from the single pixel packed sample model
	 * @param sm - sample model
	 * @param hasAlpha - true if there's an alpha channel
	 * @return LCMS format
	 */
	private static int getFormatFromSPPSampleModel(
			SinglePixelPackedSampleModel sm, boolean hasAlpha)
	{
		// Can we extract bytes?
		int mask = sm.getBitMasks()[0] >>> sm.getBitOffsets()[0];
		if (!(mask == 0xFF || mask == 0xFFFF || mask == 0xFFFFFFFF))
			return 0;

		// All masks are same?
		for (int i = 1; i < sm.getNumBands(); i++)
		{
			if ((sm.getBitMasks()[i] >>> sm.getBitOffsets()[i]) != mask)
				return 0;
		}

		int pixelSize = 4; // INT

		int bytes = 0;
		switch (mask)
		{
			case 0xFF:
				bytes = 1;
				break;
			case 0xFFFF:
				bytes = 2;
				break;
			case 0xFFFFFFFF:
				bytes = 4;
				break;
			default:
				return 0;
		}

		int channels = hasAlpha ? sm.getNumBands() - 1 : sm.getNumBands();
		int extra = hasAlpha ? 1 : 0;
		extra += pixelSize / bytes - sm.getNumBands(); // Unused bytes?

		// Form an ArrayList containing offset for each band
		ArrayList<Integer> offsetsLst = new ArrayList<Integer>();
		for (int k = 0; k < sm.getNumBands(); k++)
		{
			offsetsLst.add(new Integer(sm.getBitOffsets()[k] / (bytes * 8)));
		}

		// Add offsets for unused space
		for (int i = 0; i < pixelSize / bytes; i++)
		{
			if (offsetsLst.indexOf(new Integer(i)) < 0)
				offsetsLst.add(new Integer(i));
		}

		int offsets[] = new int[pixelSize / bytes];
		for (int i = 0; i < offsetsLst.size(); i++)
		{
			offsets[i] = offsetsLst.get(i).intValue();
		}

		int doSwap = 0;
		int swapFirst = 0;
		boolean knownFormat = false;

		int i;

		// "RGBA"
		for (i = 0; i < pixelSize; i++)
		{
			if (offsets[i] != i)
				break;
		}
		if (i == pixelSize)
		{ // Ok, it is it
			doSwap = 0;
			swapFirst = 0;
			knownFormat = true;
		}

		// "ARGB"
		if (!knownFormat)
		{
			for (i = 0; i < pixelSize - 1; i++)
			{
				if (offsets[i] != i + 1)
					break;
			}
			if (offsets[i] == 0)
				i++;
			if (i == pixelSize)
			{ // Ok, it is it
				doSwap = 0;
				swapFirst = 1;
				knownFormat = true;
			}
		}

		// "BGRA"
		if (!knownFormat)
		{
			for (i = 0; i < pixelSize - 1; i++)
			{
				if (offsets[i] != pixelSize - 2 - i)
					break;
			}
			if (offsets[i] == pixelSize - 1)
				i++;
			if (i == pixelSize)
			{ // Ok, it is it
				doSwap = 1;
				swapFirst = 1;
				knownFormat = true;
			}
		}

		// "ABGR"
		if (!knownFormat)
		{
			for (i = 0; i < pixelSize; i++)
			{
				if (offsets[i] != pixelSize - 1 - i)
					break;
			}
			if (i == pixelSize)
			{ // Ok, it is it
				doSwap = 1;
				swapFirst = 0;
				knownFormat = true;
			}
		}

		// XXX - Planar formats are not supported yet
		if (!knownFormat)
			return 0;

		return channelsSh(channels) | bytesSh(bytes) | extraSh(extra)
				| doswapSh(doSwap) | swapfirstSh(swapFirst);
	}

	/**
	 * Obtains data array from the DataBuffer object
	 * @param db - data buffer
	 * @return - true if successful
	 */
	private boolean setImageData(int[] db)
	{
		imageData = db;
		return true;
	}

	/**
	 * Calculates scanline stride in bytes
	 * @param sppsm - sample model
	 * @param r - raster
	 * @return scanline stride in bytes
	 */
	private static int calculateScanlineStrideSPPSM(
			SinglePixelPackedSampleModel sppsm, Raster r)
	{
		if (sppsm.getScanlineStride() != sppsm.getWidth())
		{
			int dataTypeSize = 32 / 8;
			return sppsm.getScanlineStride() * dataTypeSize;
		}
		return -1;
	}

	/**
	 * Calculates byte offset of the alpha channel from the beginning of the pixel data
	 * @param sm - sample model
	 * @param r - raster
	 * @return byte offset of the alpha channel
	 */
	private static int calculateAlphaOffset(SampleModel sm, Raster r)
	{
		if (sm instanceof SinglePixelPackedSampleModel)
		{
			SinglePixelPackedSampleModel sppsm = (SinglePixelPackedSampleModel) sm;
			return sppsm.getBitOffsets()[sppsm.getBitOffsets().length - 1] / 8;
		}
		else
		{
			return -1; // No offset, don't copy alpha
		}
	}
}
